var template = `
<div class="runner">
    <hr/>
    <div v-if="banMsgFar">{{banMsgFar}}</div>
    <div v-if="sharedState.runTask">
        runTask
        <div v-if="runTask" class="form">
            <div v-for="key in Object.keys(showModel)">
                <div class="label"><label>{{doGetKeyFar(key)}}:</label></div>
                <div class="input">{{doFormatFar(runTask,key)}}</div>
                <div style="clear:both;"></div>
            </div>
        </div>
    </div>
    <div class="no-data" v-else>没有数据</div>
</div>
`;

let RunnerFar = {
    name: 'RunnerFar',
    components: {},
    template,
    props: {},
    data() {
        return {
            sharedState: store.state,
            punishMs: appConfig.heartBeatPunish * 1000,
            lastTimeOutId: -1,
            timeoutMs: appConfig.heartBeatFixedDelay * 1000,
            lastSuccessTime: Date.now(),
            lastCostTime: -1,
            exit: false,
            sleepTimeMs: 10,
            runTask: null,
            nextList: [],
            nextModel: {
                // 下一个方法名
                name: '',
                // 下一个方法入参
                param: {}
            },
            // 任务栈计数
            nextCount: 0,
            // 任务栈空闲计数 空闲10次
            nextNothingCount: 0,
            runnerId: UUID(),
            config: {},
            isUserCheck: false,
            isUserOk: false,
            isProxyOk: false,
            // 任务是否正常
            isTaskOk: false,
            taskFailCount: 0,
            taskCheckUrl: '',
            sharedData: null,
            username: '',
            current: -1,
            currentUrl: '',
            total: -1,
            tempPost: null,
            tempPostEdges: null,
            tempPostEdgesSize: null,
            tempPostEdgesIndex: 0,
            tempPostEdgesFailCount: 0,
            tempPicImagePaths: [],
            tempPicList: [],
            ignoreGetImage: appConfig.ignoreGetImage,
            updateEdgeList: [],
            updateEdgeListIndex: -1,
            updateEdgeListSize: -1,
            schema: '任务信息',
            showModel: {
                taskName: '',
                currentUrl: '',
                // threadSize:'',
                // first:'',
                total: '',
                current: '',
                percentSuccess: '',
                // timeoutExecTime:'',
                // runningThread:'',
                running: '',
                paused: '',
                firstExecTime: {
                    type: 'date',
                    formatter: 'yyyy-MM-dd HH:mm:ss'
                },
                lastExecTime: {
                    type: 'date',
                    formatter: 'yyyy-MM-dd HH:mm:ss'
                },
                latestActiveTime: {
                    type: 'date',
                    formatter: 'yyyy-MM-dd HH:mm:ss'
                },
                createdTime: {
                    type: 'date',
                    formatter: 'yyyy-MM-dd HH:mm:ss'
                },
                updatedTime: {
                    type: 'date',
                    formatter: 'yyyy-MM-dd HH:mm:ss'
                },
            },
            banMsgFar: '',
            // url是否是商品详情页
            isItem: '-track-',
            dp: new DOMParser(),
            // 商品信息
            itemData: null,
        }

    },
    methods: {
        // 初始化
        doGetKeyFar(key = '') {
            let {apiDocs} = this.sharedState;
            if (apiDocs) {
                let {schemas = {}} = apiDocs.components;
                let schemaObj = schemas[this.schema];
                if (schemaObj) {
                    return schemaObj.properties && schemaObj.properties[key] && schemaObj.properties[key].description || key;
                }
            }
            return key;
        },
        // 格式化
        doFormatFar(item, key) {
            if (this.showModel[key] && this.showModel[key].formatter) {
                if ('date' === this.showModel[key].type) {
                    return new Date(item[key]).fmt(this.showModel[key].formatter);
                } else {
                    return item[key];
                }
            } else if ('text' === this.showModel[key].type) {
                if (this.showModel[key].maxLength && this.showModel[key].maxLength > 0) {
                    let maxLength = this.showModel[key].maxLength;
                    if (typeof (item[key]) === 'string' && item[key].length > maxLength) {
                        return item[key].substr(0, maxLength) + '...';
                    } else {
                        return item[key];
                    }
                } else {
                    return item[key];
                }
            } else {
                return item[key];
            }
        },
        // 格式化
        async doInitFar() {
            let {runTask} = this.sharedState;
            if (!runTask) {
                return;
            }
            this.runTask = runTask;
            let {firstExecTime = 0} = runTask;
            if (firstExecTime <= 0) {
                firstExecTime = Date.now();
                runTask.firstExecTime = firstExecTime;
            }
            runTask.lastExecTime = Date.now();
            let config = await this.doGetConfigFar();
            let total = await this.doGetTotalFar();
            let left = await this.doGetLeftFar();
            if(total > 0 && left === 0){
                // 已经没有了, 重新下载所有链接
                console.log('已经没有了, 重新下载所有链接');
                let resReset = await this.doResetFar();
                console.log('重新下载所有链接', resReset);
            }
            let current = await this.doGetCurrentFar();
            let itemTotal = await this.doGetItemTotalFar();
            this.total = total;
            runTask.total = total;
            runTask.current = current;
            runTask.percentSuccess = current / total * 100;
            runTask.itemTotal = itemTotal;
            this.config = config;
            let {isUserCheck = false} = config;
            let {sleepTaskMs} = config;
            sleepTaskMs = sleepTaskMs > 0 ? sleepTaskMs : appConfig.defaultSleepTaskMs;
            let lastTaskMs = localStorage.getItem("lastTaskMs");
            if (lastTaskMs && Date.now() - Number(lastTaskMs) < sleepTaskMs) {
                let msg = ['任务启动失败!', `上次任务过去时间不够久 ${new Date(Number(lastTaskMs)).fmt()}`];
                console.warn(...msg);
                this.$message({message: msg});
                this.doStopFar();
                return;
            }
            this.isUserCheck = isUserCheck;
            if (!isUserCheck) {
                this.isUserOk = true;
            }
            let {sleepTimeMs = 500} = config;
            this.sleepTimeMs = sleepTimeMs;
            this.timeoutMs = sleepTimeMs;
            return this.doStartFar();
        },
        // 获取配置
        async doStartFar() {
            let {runTask} = this.sharedState;
            if (!runTask) {
                let msg = ['任务启动异常!', 'runTask:' + typeof (runTask)];
                console.error(...msg);
                this.$message({message: msg, level: 'error'});
                this.doStopFar();
                return;
            }
            let {taskName = ''} = runTask;
            this.$message({message: ['任务开始', taskName]});
            let startCount = 0;
            while (!this.exit) {
                if (this.sleepTimeMs <= 100) {
                    this.sleepTimeMs = 100;
                }
                await this.sleepFar(this.sleepTimeMs);
                // ------
                // ------
                // ------ 局部函数 ------
                let isPausedFar = async () => {
                    let start = Date.now();
                    while (runTask.paused && runTask.running) {
                        await this.sleepFar(this.sleepTimeMs);
                        let past = Date.now() - start;
                        if (past / 1000 > 30) {
                            let msg = [`任务暂停中 ${this.runnerId}`, `current=${this.current} cid=${this.currentUrl}`];
                            console.log(...msg);
                            this.$message({message: msg});
                            start = Date.now();
                        }
                    }
                };
                let isFinishedFar = () => this.current + 1 === this.total && this.total > 0;
                let isRunningFar = () => runTask.running && !appConfig.allStop;
                let isTaskIntervalFar = () => 0;
                // ------
                // ------
                // ========
                // ========
                // ======== 任务执行
                // ========
                // ========
                // ========
                // ========
                if (isFinishedFar()) {
                    localStorage.setItem("lastTaskMs", String(Date.now()));
                    this.$message();
                    this.doStopFar();
                    this.exit = true;
                    break;
                }
                // =====  检查用户
                if (this.isUserCheck) {
                    /*
                    检查用户登录
                     */
                }
                // ====== 获取登录信息
                // await isPausedFar();
                // try {
                //                //     runTask.latestActiveTime = Date.now();
                //     await this.doGetSharedDataFar();
                // } catch (e) {
                //                //     let msg = ['获取登录信息失败!'];
                //     this.$message({message: msg, level: 'error'});
                //     console.log(...msg, e);
                //     this.exit = true;
                //     this.doStopFar();
                //     continue;
                // }
                // ========= 检查登录信息
                // await isPausedFar();
                // try {
                //                //     runTask.latestActiveTime = Date.now();
                //     await this.doCheckTaskFar();
                // } catch (e) {
                //                //     let msg = ['检查任务失败!', e];
                //     console.log(...msg);
                //     this.$message({message: msg, level: 'error'});
                //     this.exit = true;
                //     this.doStopFar();
                //     continue;
                // }
                // ==========  执行任务
                if (!runTask.running) {
                    this.doStopFar();
                    continue;
                }
                if (runTask.paused) {
                    continue;
                }
                // ====== 一个执行节点
                /*
                获取页面源码
                 */
                await isPausedFar();
                try {
                    runTask.latestActiveTime = Date.now();
                    await this.doGetHtmlFar();
                } catch (e) {
                    console.log('获取页面源码失败! url=' + this.currentUrl, e);
                    await this.doPunishFar(this.punishMs);
                    continue;
                }
                /*
                获取页面源码图片及更新
                 */
                while (this.current !== -1) {
                    await this.sleepFar(this.sleepTimeMs);
                    await isPausedFar();
                    if (!isRunningFar()) {
                        break;
                    }
                    try {
                        runTask.latestActiveTime = Date.now();
                        await this.doGetPicFar();
                    } catch (e) {
                        console.log('下载图片失败! uid=' + this.currentUrl, e);
                        await this.doPunishFar(this.punishMs);
                    }
                }
                /*
                上传未传完的页面源码数据
                 */
                // this.tempPostEdgesFailCount = 0;
                // this.updateEdgeListIndex = 0;
                // this.updateEdgeListSize = this.updateEdgeList.length;
                // while (this.updateEdgeList.length > 0) {
                //     await this.sleepFar(this.sleepTimeMs);
                //     await isPausedFar();
                //     if (!isRunningFar()) {
                //         break;
                //     }
                //     try {
                //         runTask.latestActiveTime = Date.now();
                //         await this.doUpdateEdgeFar();
                //     } catch (e) {
                //         console.log('上传页面源码失败! uid=' + this.currentUrl, e);
                //         await this.doPunishFar(this.punishMs);
                //     }
                // }

                if (!isRunningFar()) {
                    console.log('一次循环执行完毕', startCount++);
                    break;
                }
                this.updateFar();
            }
            this.doStopFar();
            console.log('任务退出!')
        },
        async doPunishFar(timeMs = 0) {
            this.timeoutMs = this.timeoutMs + this.punishMs;
            await this.sleepFar(this.timeoutMs);
            this.timeoutMs = 0;
        },
        updateFar() {
            let {runTask} = this.sharedState;
            if (runTask) {
                console.log('更新任务');
                this.$emit('update', runTask);
            }
        },
        doGetTotalFar() {
            return new Promise(async (resolve, reject) => {
                let param = {name: tbl.url}
                await api.mongo.count(param).then(res => {
                    if (res.data && res.data.code === 0) {
                        let total = res.data.data;
                        resolve(total);
                        return;
                    }
                    console.error("读取链接总数失败!", res);
                    resolve(0);
                }).catch(e => {
                    console.error("读取链接总数失败!", e);
                    resolve(0);
                })
            })
        },
        /**
         * 获取未下载的链接数量
         * @returns {Promise<int>}
         */
        doGetLeftFar() {
            return new Promise(async (resolve, reject) => {
                let param = {name: tbl.url,status: null}
                await api.mongo.count(param).then(res => {
                    if (res.data && res.data.code === 0) {
                        let total = res.data.data;
                        resolve(total);
                        return;
                    }
                    console.error("读取未下载链接总数失败!", res);
                    resolve(-1);
                }).catch(e => {
                    console.error("读取未下载链接总数失败!", e);
                    resolve(-1);
                })
            })
        },
        doResetFar() {
            return new Promise(async (resolve, reject) => {
                let param = {
                    name: tbl.url,
                    condition: {
                       status: {$ne: null}, isItem: 0
                    },
                    update: {
                        status: null
                    }
                }
                await api.mongo.updateMany(param).then(res => {
                    if (res.data && res.data.code === 0 && res.data.data) {
                        let update = res.data.data;
                        let {matchedCount, modifiedCount} = update;
                        let msg = [`重置所有链接成功`,`匹配${matchedCount}条数据`,`更新${modifiedCount}条数据`];
                        console.log(...msg);
                        this.$message({message: msg});
                        resolve(update);
                        return;
                    }
                    console.error("重置所有链接失败!", res);
                    resolve(null);
                }).catch(e => {
                    console.error("重置所有链接失败!", e);
                    resolve(null);
                })
            })
        },
        doGetCurrentFar() {
            return new Promise(async (resolve, reject) => {
                let param = {
                    name: tbl.url,
                    status: tbs.ok
                }
                await api.mongo.countCondition(param).then(res => {
                    if (res.data && res.data.code === 0) {
                        let total = res.data.data;
                        resolve(total);
                        return;
                    }
                    console.error("读取链接下载总数失败!", res);
                    resolve(0);
                }).catch(e => {
                    console.error("读取链接下载总数失败!", e);
                    resolve(0);
                })
            })
        },
        doGetItemTotalFar() {
            return new Promise(async (resolve, reject) => {
                let param = {
                    name: tbl.url,
                    isItem: 1
                }
                await api.mongo.countCondition(param).then(res => {
                    if (res.data && res.data.code === 0) {
                        let total = res.data.data;
                        resolve(total);
                        return;
                    }
                    console.error("读取链接商品总数失败!", res);
                    resolve(0);
                }).catch(e => {
                    console.error("读取链接商品总数失败!", e);
                    resolve(0);
                })
            })
        },
        doGetConfigFar() {
            return new Promise(((resolve, reject) => {
                api.config.getAll().then(res => {
                    if (res.data && res.data.code === 0) {
                        if (res.data.data.length > 0) {
                            let config = res.data.data[0];
                            resolve(config);
                            return;
                        }
                    }
                    resolve({});
                }).catch((e) => {
                    console.error("读取配置文件失败!", e);
                    resolve({});
                })
            }))
        },
        async sleepFar(sleepMs = this.sleepTimeMs) {
            return new Promise(((resolve, reject) => {
                setTimeout(() => {
                    resolve()
                }, sleepMs)
            }))
        },
        doStopFar() {
            let {runTask} = this.sharedState;
            if (runTask) {
                runTask.paused = false;
                runTask.running = false;
                this.updateFar();
                this.sharedState.runTask = null;
                this.exit = true;
                let {taskName = ''} = runTask;
                this.$message({message: ['任务结束', taskName]});
            }
        },
        /**
         * 获取网页内容
         * @returns {Promise<unknown>}
         */
        async doGetHtmlFar() {
            return new Promise(async (resolve, reject) => {
                let {runTask} = this.sharedState;
                if (!runTask) {
                    let msg = ['获取 任务信息 异常!', 'runTask:' + typeof (runTask)];
                    console.error(...msg);
                    this.$message({message: msg, level: 'error'});
                    reject(msg);
                    return;
                }
                try {
                    let url = await this.doGetUrlNot();
                    let urlTotalNot = await this.doGetUrlTotalNot();
                    console.log('更新未下载链接总数, urlTotalNot', urlTotalNot);
                    runTask.total = urlTotalNot;
                    let current = await this.doGetCurrentFar();
                    let itemTotal = await this.doGetItemTotalFar();
                    runTask.current = current;
                    runTask.itemTotal = itemTotal;
                    runTask.percentSuccess = current / urlTotalNot * 100;
                    this.currentUrl = url;
                    if (!url) {
                        let msg = ['获取待下载url异常!', 'url:' + typeof (url)];
                        console.error(...msg);
                        this.$message({message: msg, level: 'error'});
                        reject(msg);
                        return;
                    }
                    // 获取网页内容
                    let resHtml = await axios.get(url);
                    if (resHtml.data && resHtml.data.length > 0) {
                        let html = resHtml.data;
                        let finalUrl = this.fixUrl(resHtml.finalUrl);
                        let status = resHtml.status;
                        // toLearn: 这里搞不懂, 进入一个新的作用域, url变成了undefined
                        // 已修复, 因为下面定义了一个新的变量, 变量名为url, 导致url变成了undefined
                        // 2022年4月26日, 发现这里的url是没有值的, 只有finalUrl有值
                        if (url !== finalUrl) {
                            console.log("---doGetHtmlFar---获取的url不是最终的url,将更新url! url: %s -> %s", url, finalUrl);
                            await this.doUpdateUrlStatus(url, tbs.exclude);
                            url = finalUrl;
                        }
                        let pageOrItemObj = await this.doGetPageOrItemObj(url, html);
                        let {itemData, pageData} = pageOrItemObj;
                        // 2022年6月29日, 已下架或其他原因获取到的itemMainType为未知的不保存(防止覆盖已有数据)
                        if (itemData && itemData.itemMainType !== '未知') {
                            console.log('---doGetHtmlFar---itemData: %s', itemData);
                            let resItem = await this.doUpdateItem(itemData);
                            if (resItem.data && resItem.data.code === 0) {
                                await this.doUpdateImgList(itemData);
                                let msg = ['更新item成功!', 'itemId:' + itemData.itemId];
                                console.log(...msg);
                                this.$message({message: msg, level: 'success'});
                                this.itemData = itemData;
                                this.current = 0;
                                await this.doUpdateType(itemData);
                            } else {
                                let msg = ['更新item失败!', 'itemId:' + itemData.itemId];
                                console.error(...msg);
                                this.$message({message: msg, level: 'error'});
                                reject(msg);
                                return;
                            }
                        } else {
                            this.itemData = null;
                        }
                        if (pageData) {
                            pageData.statusCode = status;
                            console.log('---doGetHtmlFar---pageData:', pageData);
                            let resPage = await this.doUpdatePage(pageData);
                            if (resPage.data && resPage.data.code === 0) {
                                let msg = ['更新page成功!', 'id:' + pageData.id];
                                console.log(...msg);
                                this.$message({message: msg, level: 'success'});
                            } else {
                                let msg = ['更新page失败!', 'id:' + pageData.id];
                                console.error(...msg);
                                this.$message({message: msg, level: 'error'});
                                reject(msg);
                                return;
                            }
                        } else {
                            console.log('---doGetHtmlFar---pageData: %s', pageData);
                            reject(null);
                            return;
                        }
                        // toLearn 这里定义url必须取一个别名, 否则导致外面的url变成undefined
                        let {id, url: pageUrl, urls} = pageData;
                        await this.doUpdateUrls(pageUrl, urls);
                        await this.doUpdateUrlOk(id);
                        console.log('---doGetHtmlFar---获取url成功! url: %s', url);
                        console.log('清除邮箱计数');
                        localStorage.setItem("emailCount", "0");
                        resolve(pageData);
                    } else {
                        let msg = ['获取网页内容异常!', 'html:' + typeof (resHtml)];
                        console.error(...msg);
                        this.$message({message: msg, level: 'error'});
                        await this.doUpdateUrlError(url);
                        reject(msg);
                    }
                } catch (e) {
                    console.error("---doGetHtmlFar---获取网页内容失败!", e);
                    let msg = ['获取网页内容失败!', 'url:' + url];
                    this.$message({message: msg, level: 'error'});
                    await this.doUpdateUrlError(url);
                    reject(e);
                }
            })
        },
        /**
         * 获取页面或商品信息
         * @param url 商品链接
         * @param html 商品页面html
         * @returns {undefined}
         */
        async doGetPageOrItemObj(url, html) {
            console.log('-----doGetPageOrItemObj-----', url);
            return new Promise(async (resolve, reject) => {
                window.html = html;
                window.url = url;
                let dom = this.dp.parseFromString(html, 'text/html');
                let z = zp(dom);
                let title = this.fixTitle(z.find('title').text());
                let lang = this.getLanguage(url);
                let id = sha1(url);
                let urls = Array.from(new Set(z.find('a').get()
                    .map(e => e.href)
                    .filter(u => u) // 去除空值
                    .map(u => this.fixUrl(u)))) // 修正url
                let itemData, pageData;
                let isItem = url.includes('-item-') ? 1 : 0;
                if (isItem) {
                    let itemOriginText = html.between('<script>window.__HYDRATION_STATE__=', '</script>', true);
                    let itemOrigin = {};
                    try {
                        let jsonText = eval(itemOriginText);
                        itemOrigin = JSON.parse(jsonText);
                    } catch (e) {
                        console.error('---doGetPageOrItemObj---获取itemOrigin失败!', e);
                    }
                    let itemId = url.split('-item-')[1].split('.')[0];
                    let brandNameEl = z.find('a[data-tstid="cardInfo-title"]').get(0);
                    let brandName = brandNameEl ? brandNameEl.innerText.trim() : '未知';  //品牌名称
                    let itemNameEl = z.find('span[data-tstid="cardInfo-description"]').get(0);
                    let itemName = itemNameEl ? itemNameEl.innerText.trim() : '未知';  //商品名称
                    let priceEl = z.find('span[data-tstid="priceInfo-original"]').get(0);
                    let price = priceEl ? priceEl.innerText.trim() : '未知';  //商品价格
                    let sizeList = z.find('span[data-tstid="sizeDescription"]').get().map(e => e.innerText.trim()); //商品尺码
                    let deliveryTime = this.getDeliveryTime(z, itemOrigin);  //商品发货时间
                    let sameStyleList = this.getSameStyleList(z);  //同款
                    let imgList = this.getImgList(z, itemOrigin);  //图片
                    let breadNavList = this.getItemBreadNavList(z);  //面包屑导航
                    let itemMainType = breadNavList.length === 4 ? breadNavList[0] : '未知';  //商品主类型
                    let itemBrand = breadNavList.length === 4 ? breadNavList[1] : '未知';  //商品品牌
                    let itemType = breadNavList.length === 4 ? breadNavList[2] : '未知';  //商品类型
                    let itemSubType = breadNavList.length === 4 ? breadNavList[3] : '未知';  //商品子类型
                    let detail = this.getItemDetail(z);  //商品详情
                    let highLightList = this.getItemHighLightList(z);  //商品高亮
                    let composition = this.getItemComposition(z);  //商品成分
                    let brandStyleId = z.find('p[data-tstid="designerStyleId"] span').text();  //品牌风格id
                    itemOrigin.id = id;
                    itemOrigin.url = url;
                    itemOrigin.status = itemOrigin['apolloInitialState'] ? tbs.ok : tbs.fail;
                    itemOrigin.itemId = itemId;
                    itemOrigin.title = title;
                    itemOrigin.itemName = itemName;
                    itemOrigin.brandName = brandName;
                    itemData = {
                        id,
                        itemId,
                        brandName,
                        itemName,
                        price,
                        sizeList,
                        deliveryTime,
                        sameStyleList,
                        imgList,
                        breadNavList,
                        itemMainType,
                        itemBrand,
                        itemType,
                        itemSubType,
                        detail,
                        highLightList,
                        composition,
                        brandStyleId,
                        lang,
                        url,
                        title,
                        itemOrigin,
                    };
                }
                pageData = {
                    id,
                    lang,
                    url,
                    title,
                    html: [html],
                    isItem,
                    urls
                };
                let data = {
                    itemData,
                    pageData
                };
                resolve(data);
            })
        },
        getImgList(z, itemOrigin = {}) {
            let list = z.find('img[data-test^="imagery-img"]').get().map(e => e.src).map((src, ind) => {
                let url = this.fixUrl(src);
                let id = sha1(url);
                let index = ind + 1;
                return {
                    id, url, index,
                }
            });
            try {
                let productViewModel = this.getProductViewModel(itemOrigin);
                list = productViewModel.images.main.map((e, ind) => {
                    let url = this.fixUrl(e['zoom']);
                    let id = sha1(url);
                    let index = e['index'] || ind + 1;
                    return {
                        id, url, index,
                    }
                });
                return list;
            } catch (e) {
                console.log(e);
                return list;
            }
        },
        getItemComposition(z) {
            let composition = '未知';
            // 英文
            if (composition === '未知') {
                // 备注: 中文与英文的结构不同
                let compositionEl = z.find('div[data-tstid="theDetails-part2"] div').get().filter(e => zp(e).text().startsWith('Composition')).map(e => zp(e).find('p').get())[0];
                composition = compositionEl ? compositionEl.map(e => e.innerText).join(' ') : '未知';  //商品成分
            }
            // 中文
            if (composition === '未知') {
                // 备注: 中文与英文的结构不同
                let compositionEl = z.find('div[data-tstid="theDetails-part2"] div').get().filter(e => zp(e).text().startsWith('成分')).map(e => zp(e).find('p').get())[0];
                composition = compositionEl ? compositionEl.map(e => e.innerText).join(' ') : '未知';  //商品成分
            }
            return composition;
        },
        getItemHighLightList(z) {
            let highLightList = [];
            // 英文
            if (highLightList.length === 0) {
                // 备注: 中文与英文的结构不同
                let highLightEl = z.find('div').get().filter(el => zp(el).find('h4').get(0)).filter(el => zp(el).text().startsWith('Highlights'))[0];
                highLightList = highLightEl ? zp(highLightEl).find('li').get().map(e => e.innerText.trim()) : [];  //商品亮点
            }
            // 中文
            if (highLightList.length === 0) {
                // 备注: 中文与英文的结构不同
                let highLightEl = z.find('div').get().filter(el => zp(el).find('h4').get(0)).filter(el => zp(el).text().startsWith('设计亮点'))[0];
                highLightList = highLightEl ? zp(highLightEl).find('li').get().map(e => e.innerText.trim()) : [];  //商品亮点
            }
            return highLightList;
        },
        getItemDetail(z) {
            let detail = '未知';
            // 英文
            if (detail === '未知') {
                // 备注: 中文与英文的结构不同
                let detailEl = z.find('div[data-tstid="productDetails"] p[class*="-Body"]').get(0);
                detail = detailEl ? detailEl.innerText.trim() : '未知';  //商品详情
            }
            // 中文
            if (detail === '未知') {
                // 备注: 中文与英文的结构不同
                let detailEl = z.find('p[data-tstid="fullDescription"]').get(0);
                detail = detailEl ? detailEl.innerText.trim() : '未知';  //商品详情
            }
            return detail;
        },
        getItemBreadNavList(z) {
            return z.find('span[itemprop="name"]').get().map((el, ind) => {
                let breadName = el.innerText.trim();
                let url = this.fixUrl(el.parentElement.href);
                return {breadName, url, level: ind + 1}
            })
        },
        getSameStyleList(z) {
            return z.find('div[data-tstid="sameStyleSlider"] a').get().map(a => {
                let url = this.fixUrl(a.href);
                let itemId = sha1(url);
                let imgEl = zp(a).find('img').get(0)
                let imgUrl, imgId, itemName = ''
                if (imgEl) {
                    imgUrl = this.fixUrl(imgEl.src);
                    imgId = imgUrl ? sha1(imgUrl) : '';
                    itemName = zp(imgEl).attr('alt') || '';
                }
                return {
                    itemId, url, imgId, imgUrl, itemName
                }
            });
        },
        getDeliveryTime(z, itemOrigin = {}) {
            // 不准切 z.find('span[dir="ltr"]').last().text()
            let deliveryTime = '未知';
            // 英文
            if (deliveryTime === '未知') {
                // 备注: 中文与英文的结构不同
                let deliveryTimeEl = z.find('p').get().filter(p => p.innerText === 'Estimated delivery')[0];
                deliveryTime = deliveryTimeEl ? deliveryTimeEl.nextElementSibling.innerText.trim() : '未知';  //商品详情
            }
            // 中文
            if (deliveryTime === '未知') {
                // 备注: 中文与英文的结构不同
                let deliveryTimeEl = z.find('p').get().filter(p => p.innerText === '预计送达时间')[0];
                deliveryTime = deliveryTimeEl ? deliveryTimeEl.nextElementSibling.innerText.trim() : '未知';  //商品详情
            }
            if (deliveryTime === '未知') {
                try {
                    let productViewModel = this.getProductViewModel(itemOrigin);
                    let eddExpress = productViewModel['shippingInformations']['details']['default']['eddExpress'];
                    deliveryTime = eddExpress ? eddExpress.trim() : '未知';
                } catch (e) {
                    console.log(e);
                }
            }
            return deliveryTime;
        },
        getProductViewModel(itemOrigin) {
            try {
                let rootQuery = itemOrigin['apolloInitialState']['ROOT_QUERY'] || {};
                let initKey = Object.keys(rootQuery).filter(k => typeof (rootQuery[k]) !== 'string')[0] || Object.keys(rootQuery).filter(k => k !== '__typename')[0];
                let data = rootQuery[initKey].data || {};
                let productViewModel = data['slice-product']['productViewModel'] || {};
                if (productViewModel) {
                    return productViewModel;
                }
            } catch (e) {
                console.log(e);
            }
            return {};
        },
        getLanguage(url) {
            let lang = 'en';
            if (url.indexOf('/cn/') > -1) {
                lang = 'cn';
            }
            return lang;
        },
        fixUrl(url = '') {
            url = url.split('#')[0]; // 去掉hash
            // 保留参数 url = url.split('?')[0]; // 去掉参数
            // 修正域名
            url = url.startsWith('https://www.farfetch.cn/') ? url : url.replace(window.location.href.split('#')[0], 'https://www.farfetch.cn/');
            return url;
        },
        fixTitle(title) {
            let arr = title.split('-');
            if (arr.length > 1) {
                arr.pop();
            }
            return arr.join('-').trim();
        },
        /**
         * 下载页面图片数据
         * @returns {Promise<unknown>}
         */
        doGetPicFar() {
            return new Promise(async (resolve, reject) => {
                if (!this.itemData) {
                    this.current = -1
                    resolve([]);
                    return;
                }
                let {id: itemBaseId, itemId, url: itemUrl, imgList = [], title} = this.itemData;
                for (let i = 0; i < imgList.length; i++) {
                    const img = imgList[i];
                    let {id: imgId, url, index} = img;
                    if (url) {
                        await axios.get(url, {responseType: 'blob'}).then(async res => {
                            if (res.status === 200) {
                                let blob = res.data;
                                // 文件名规范: data-[itemId]-[imgId].jpg; 必须 data- 开头, 中间用 - 分隔, 只有两个 - 分隔;
                                let fileName = `data-${itemId}-${index}_${imgId}.jpg`;
                                let fileDiskName = `${itemId}/${index}_${imgId}.jpg`;
                                let size = blob.size;
                                let base64 = await blob64.serializePromise(blob);
                                /*id,url,index,itemBaseId,itemId,itemUrl,title*/
                                let imgData = {
                                    id: imgId,
                                    url,
                                    index,
                                    itemBaseId,
                                    itemId,
                                    itemUrl,
                                    title,
                                    fileName,
                                    fileDiskName,
                                    size,
                                    // base64, // 原始base64, 不需要
                                    status: tbs.ok,
                                };
                                /*id,url,index,itemBaseId,itemId,itemUrl,title*/
                                await this.doUpdateImgOk(imgData);
                                await this.doUploadImg(blob, fileName);
                                let msg = ['下载图片成功', `${i + 1} of ${imgList.length}`, '图片大小: ' + size];
                                console.log('---doGetPicFar---', ...msg);
                                this.$message({message: msg, level: 'success'});
                                await this.sleepFar(this.sleepTimeMs);
                            } else {
                                let msg = ['下载图片失败', `状态码错误[${res.status}]`, `${url} 请求失败`];
                                console.log(...msg);
                                this.$message({message: msg, level: 'error'});
                                await this.doUpdateImgErr(imgId);
                                reject(msg);
                            }
                        }).catch(async err => {
                            console.log(err);
                            let msg = ['下载图片失败', `${i} of ${imgList.length}`, err.message];
                            console.log(...msg);
                            this.$message({message: msg, level: 'error'});
                            await this.doUpdateImgErr(imgId);
                            reject(err);
                        })
                    }
                }
                this.current = -1;
                resolve('---doGetPicFar---success!');
            })
        },
        async doUpdateItem(itemData) {
            return new Promise(async (resolve, reject) => {
                let param = {
                    name: tbl.item,
                }
                Object.assign(param, itemData);
                delete param.itemOrigin;
                let itemOrigin = itemData.itemOrigin;
                await this.doUpdateItemOrigin(itemOrigin);
                let resUpdateItem = await api.mongo.save(param);
                resolve(resUpdateItem);
            })
        },
        doUpdatePage(pageData) {
            return new Promise(async (resolve, reject) => {
                let param = {
                    name: tbl.page,
                }
                Object.assign(param, pageData);
                let resUpdatePage = await api.mongo.save(param);
                resolve(resUpdatePage);
            })
        },
        doUpdateUrls(url, urls) {
            return new Promise(async (resolve, reject) => {
                let param = {
                    name: tbl.url,
                }
                let data = urls.map(u => {
                    let id = sha1(u);
                    let isItem = u.includes('-item-') ? 1 : 0;
                    let urlData = {id, url: u, from: url, isItem};
                    // 排除无效的url, 设置无效的url状态为 3 - 排除
                    if (!u.startsWith('https://www.farfetch.cn/cn/')) {
                        urlData.status = tbs.exclude;
                    }
                    return urlData;
                });
                await api.mongo.saveMany([param, data]).then(res => {
                    if (res.data && res.data.code === 0) {
                        console.log("---urls---更新成功", res, "更新数量", data.length);
                        resolve(res.data.data);
                    } else {
                        console.error("---urls---更新失败", res);
                        reject(res);
                    }
                }).catch(err => {
                    console.error("---urls---更新失败", err);
                    reject(err);
                })
            });
        },
        doUpdateUrlOk(id) {
            return new Promise(async (resolve, reject) => {
                let param = {
                    name: tbl.url,
                    id,
                    status: tbs.ok,
                };
                await api.mongo.save(param).then(res => {
                    if (res.data && res.data.code === 0) {
                        console.log("---doUpdateUrlOk---更新成功", res, id);
                        resolve(res.data.data);
                    } else {
                        console.error("---doUpdateUrlOk---更新失败", res, id);
                        reject(res);
                    }
                }).catch(err => {
                    console.error("---doUpdateUrlOk---更新失败", err, id);
                    reject(err);
                })
            });
        },
        doUpdateUrlStatus(url, status) {
            return new Promise(async (resolve, reject) => {
                let id = sha1(url);
                // 获取tbs对象的所有属性值
                let allStatus = Object.values(tbs);
                if (!allStatus.includes(status)) {
                    console.error("---doUpdateUrlStatus---status不存在", status);
                    reject(`status: ${status} 不在枚举中`);
                    return;
                }
                let param = {
                    name: tbl.url,
                    id,
                    status: status,
                };
                await api.mongo.save(param).then(res => {
                    if (res.data && res.data.code === 0) {
                        console.log("---doUpdateUrlStatus---更新成功", res, id, status);
                        resolve(res.data.data);
                    } else {
                        console.error("---doUpdateUrlStatus---更新失败", res, id, status);
                        reject(res);
                    }
                }).catch(err => {
                    console.error("---doUpdateUrlStatus---更新失败", err, id, status);
                    reject(err);
                })
            });
        },
        doUpdateUrlError(url) {
            return new Promise(async (resolve, reject) => {
                let param = {
                    name: tbl.url,
                    id: sha1(url),
                    url,
                    status: tbs.fail,
                };
                await api.mongo.save(param).then(res => {
                    if (res.data && res.data.code === 0) {
                        console.log("---doUpdateUrlError---更新成功", res, url);
                        resolve(res.data.data);
                    } else {
                        console.error("---doUpdateUrlError---更新失败", res, url);
                        reject(res);
                    }
                }).catch(err => {
                    console.error("---doUpdateUrlError---更新失败", err, url);
                    reject(err);
                })
            })
        },
        doGetUrlNot() {
            return new Promise(async (resolve, reject) => {
                // 获取一个衣服未下载链接
                let paramClothUrl = {
                    name: tbl.url,
                    start: 1,
                    size: 1,
                    status: tbs.not,
                    from: {[tbc.like]: '/clothing-'}
                };
                // 获取一个 衣服列表未下载链接
                let paramClothListUrl = {
                    name: tbl.url,
                    start: 1,
                    size: 1,
                    status: tbs.not,
                    url: {[tbc.like]: '/clothing-'}
                };
                // 获取任意一个未下载链接
                let paramAnyUrl = {
                    name: tbl.url,
                    start: 1,
                    size: 1,
                    status: tbs.not,
                    url: {[tbc.notIn]: [null, '']}
                };
                let resPageNot;
                // 优先获取一个衣服未下载链接,如果没有,则获取一个衣服列表未下载链接,如果没有,则获取任意一个未下载链接
                resPageNot = await api.mongo.find(paramClothUrl);
                if (!(resPageNot.data && resPageNot.data.code === 0 && resPageNot.data.data.length > 0)) {
                    console.log("---doGetUrlNot---没有衣服未下载链接");
                    resPageNot = await api.mongo.find(paramClothListUrl);
                } else {
                    console.log("---doGetUrlNot---有衣服未下载链接", resPageNot.data.data[0].url);
                }
                if (!(resPageNot.data && resPageNot.data.code === 0 && resPageNot.data.data.length > 0)) {
                    console.log("---doGetUrlNot---没有衣服列表未下载链接");
                    resPageNot = await api.mongo.find(paramAnyUrl);
                }
                let data = {};
                if (resPageNot.data && resPageNot.data.code === 0) {
                    let list = resPageNot.data.data;
                    if (list && list.length > 0) {
                        data = list[0];
                    } else {
                        data.url = 'https://www.farfetch.cn/cn/';
                        console.log("---doGetHtmlFar---没有链接可以获取! 将获取首页链接!");
                    }
                } else {
                    console.error("---doGetUrlNot---获取失败", resPageNot);
                    resolve('');
                    return;
                }
                let {url = '', id = ''} = data;
                url = this.fixUrl(url);
                if (url) {
                    resolve(url);
                } else {
                    resolve('');
                }
            })
        },
        doGetUrlTotalNot() {
            return new Promise(async (resolve, reject) => {
                // 获取一个链接
                let param = {
                    name: tbl.url,
                    status: tbs.not,
                    url: {[tbc.notIn]: [null, '']}
                };
                let resCount = await api.mongo.countCondition(param);
                if (resCount.data && resCount.data.code === 0) {
                    let count = resCount.data.data;
                    resolve(count);
                } else {
                    console.error("---doGetUrlNot---获取失败", resCount);
                    reject(resCount);
                }
            })
        },
        doUpdateImgList(itemData) {
            return new Promise(async (resolve, reject) => {
                let {imgList, id: itemBaseId, itemId, url: itemUrl, title} = itemData;
                let param = {
                    name: tbl.img,
                };
                let data = imgList.map(img => {
                    let {id, url, index} = img;
                    return {
                        id,
                        url,
                        index,
                        itemBaseId,
                        itemId,
                        itemUrl,
                        title
                    }
                });
                await api.mongo.saveMany([param, data]).then(res => {
                    if (res.data && res.data.code === 0) {
                        resolve(res);
                    } else {
                        console.error("---doUpdateImgNot---保存失败", res);
                        reject(res);
                    }
                }).catch(err => {
                    console.error("---doUpdateImgNot---保存失败", err);
                    reject(err);
                })
            })
        },
        doUpdateItemOrigin(itemOrigin) {
            return new Promise(async (resolve, reject) => {
                let {id = '', url = ''} = itemOrigin;
                if (!id || !url) {
                    resolve('');
                    return;
                }
                let param = {
                    name: tbl.itemOrigin,
                };
                Object.assign(param, itemOrigin);
                await api.mongo.save(param).then(res => {
                    if (res.data && res.data.code === 0) {
                        console.log("---doUpdateItemOrigin---更新成功", res, itemOrigin);
                        resolve(res.data.data);
                    } else {
                        console.error("---doUpdateItemOrigin---更新失败", res, itemOrigin);
                        reject(res);
                    }
                }).catch(err => {
                    console.error("---doUpdateItemOrigin---更新失败", err, itemOrigin);
                    reject(err);
                })
            })
        },
        doUpdateImgOk(imgData) {
            return new Promise(async (resolve, reject) => {
                let param = {
                    name: tbl.img,
                }
                Object.assign(param, imgData);
                let resUpdateImg = await api.mongo.save(param);
                resolve(resUpdateImg);
            })
        },
        doUpdateImgErr(imgId) {
            return new Promise(async (resolve, reject) => {
                let param = {
                    name: tbl.img,
                    id: imgId
                }
                let resUpdateImgErr = await api.mongo.save(param);
                resolve(resUpdateImgErr);
            })
        },
        doUploadImg(blob, fileName) {
            return new Promise((resolve, reject) => {
                let fd = new FormData();
                fd.append('file', blob, fileName);
                let gmParam = {
                    url: appConfig.host + 'file',
                    method: 'post',
                    data: fd,
                    overrideMimeType: 'multipart/form-data',
                    onload: (resUp) => {
                        if (resUp.status === 200 && resUp.responseText.includes('upFile : true')) {
                            console.log('---doUploadImg---上传图片成功!', '');
                        } else {
                            console.log('---doUploadImg---上传图片失败!');
                            reject(resUp);
                            return;
                        }
                        resolve(resUp);
                    },
                    onerror: (eUp) => {
                        console.log('---doUploadImg---上传图片时发生错误!', eUp);
                        reject(eUp);
                    }
                };
                GM_xmlhttpRequest(gmParam);
            });
        },
        doUpdateType(itemData) {
            return new Promise(async (resolve, reject) => {
                let {itemMainType = '未知', itemType = '未知', itemSubType = '未知'} = itemData;
                if (!itemMainType || !itemType || !itemSubType) {
                    resolve('');
                    return;
                }
                if (itemMainType === '未知' || itemType === '未知' || itemSubType === '未知') {
                    resolve('');
                    return;
                }
                if (!itemMainType.breadName || !itemType.breadName || !itemSubType.breadName) {
                    resolve('');
                    return;
                }
                let param = {
                    name: tbl.itemType,
                };
                let typeLevel = itemMainType.breadName + '>>' + itemType.breadName + '>>' + itemSubType.breadName;
                let id = sha1(typeLevel);
                let typeData = {
                    id,
                    itemMainType,
                    itemType,
                    itemSubType,
                    typeLevel,
                };
                Object.assign(param, typeData);
                await api.mongo.save(param).then(res => {
                    if (res.data && res.data.code === 0) {
                        console.log("---doUpdateType---更新成功", res, itemData);
                        resolve(res.data.data);
                    } else {
                        console.error("---doUpdateType---更新失败", res, itemData);
                        reject(res);
                    }
                }).catch(err => {
                    console.error("---doUpdateType---更新失败", err, itemData);
                    reject(err);
                })
            })
        },
    },
    mounted() {
        console.log('document.cookie', document.cookie);
        console.log(this.$options.name + ' mounted', this.runnerId);
        return this.doInitFar();
    },
    unmounted() {
        console.log(this.$options.name + ' unmounted', this.runnerId);
        this.exit = true;
        this.updateFar();
    },
};